package org.gap.jseed;

import static org.gap.jseed.JavaWriter.asString;
import static org.gap.jseed.JavaWriter.assignTo;
import static org.gap.jseed.JavaWriter.braces;
import static org.gap.jseed.JavaWriter.call;
import static org.gap.jseed.JavaWriter.getField;
import static org.gap.jseed.JavaWriter.instantiate;
import static org.gap.jseed.JavaWriter.line;
import static org.gap.jseed.JavaWriter.tryCatch;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.NotFoundException;

public class FieldInjector extends AbstractInjector {
	private Map<Class<? extends Annotation>, Class<? extends FieldHandler>> annotations;	

	public FieldInjector() {
		annotations = new HashMap<Class<? extends Annotation>, Class<? extends FieldHandler>>();
	}
	
	@Override
	public void injectBehavior(Class<?> theInterface, CtClass type, CtClass implementation)
			throws ClassNotFoundException, CannotCompileException,
			NotFoundException {
		Map<Class<?>, String> handlers = new HashMap<Class<?>, String>();
		
		
		StringBuffer buffer = new StringBuffer();
		for (CtField each : type.getDeclaredFields()) {
			for (Class<? extends FieldHandler> handler : getHandlers(each)) {
				String name = getHandlerName(handlers, handler);
				buffer.append(
						line(getField(type.getName(), each)) + 
						line(assignTo(each.getName(), cast(each.getType()) + call("inject", name, asString(each.getName()), "field")))
				);
			}			
		}
		if (!handlers.isEmpty()) {
			StringBuffer createdHandlers = new StringBuffer();
			for (Class<?> each : handlers.keySet()) {
				createdHandlers.append(	writeFieldAssignment(handlers, each) );			
			}
			String body = writeTryCatchOfFieldInvocations(type, buffer,	createdHandlers);
			CtConstructor constructor = new CtConstructor(new CtClass[0], implementation);
			constructor.setBody(body);
			implementation.addConstructor(constructor);
		}
	}

	private String writeFieldAssignment(Map<Class<?>, String> handlers,
			Class<?> each) {
		return line( assignTo(each.getName() + " " + handlers.get(each), instantiate(each)) );
	}

	private String writeTryCatchOfFieldInvocations(CtClass type,
			StringBuffer buffer, StringBuffer createdHandlers) {
		String body = 
			braces(
				createdHandlers.toString() + 
				tryCatch(
						"Failed to extract field from " + type.getName(),
						Field.class.getName() + " field = null", 
						buffer.toString()
				)
			);
		return body;
	}

	private String cast(CtClass type) {
		return "(" + type.getName() + ")";
	}

	private String getHandlerName(Map<Class<?>, String> handlers,
			Class<? extends FieldHandler> handler) {
		String result = new String();
		if (handlers.containsKey(handler)) {
			result = handlers.get(handler);
		} else {
			result = handler.getSimpleName();
			result = Character.toLowerCase(result.charAt(0)) + result.substring(1);
			handlers.put(handler, result);
		}
		
		return result;
	}

	/**
	 * Extract all the handlers.
	 * @param field
	 * @return
	 * @throws ClassNotFoundException
	 */
	private Collection<Class<? extends FieldHandler>> getHandlers(CtField field) throws ClassNotFoundException {
		List<Class<? extends FieldHandler>> result = new LinkedList<Class<? extends FieldHandler>>();
		for (Object each : field.getAnnotations()) {
			for (Class<? extends Annotation> eachAnnotation : annotations.keySet()) {
				if (eachAnnotation.isInstance(each)) {
					result.add(annotations.get(eachAnnotation));
				}
			}
		}
		return result;
	}
	
	public void add(Class<? extends Annotation> annotation, Class<? extends FieldHandler> handler) {
		annotations.put(annotation, handler);
	}
}
